<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations\parsers\state;

#use \ReflectionClass;

require_once 'test/testHelper.php';

class StateParserTest extends TestCase
{
	const TEST = true;
	
	/**
	 * @var ReflectionObject
	 */
	private $reflection;
	
	/**
	 * @var ArrayParser
	 */
	private $arrayParser;
	
	protected function beforeTest()
	{
		$this->arrayParser = $this->mock('ArrayParser');
		
		$this->parser = new StateParser(array(
			ParserState::ANNOTATION_VALUE => new AnnotationValueProcessor(
				$this->arrayParser)
		));
	}
	
	function testChangeStateThrowsExceptionIfTryingToSetStateToUnknownValue()
	{
		$this->setExpectedException('ParserException');
		
		$this->parser->changeState('hopefullyIAmNotAValidState');
	}
	
	function testParseDocBlockThrowsExceptionIfGivenStringIsNotProperlyEnclosed()
	{
		$this->setExpectedException('ParserException');
		
		$this->parser->parseDocBlock('/* foo */');
	}
	
	function testParseDocBlockThrowsExceptionIfTagNameDoesNotStartWithAlphabeticChar()
	{
		$this->setExpectedException('ParserException');
		
		$this->parser->parseDocBlock('/** @7es7 */');
	}
	
	function testParseDocBlockThrowsExceptionIfEndsWithStateOtherThanDocBlock()
	{
		$tests = array(
			'@Fake(',
			'@Fake<Type',
			'@Fake[param'
		);
		
		$parser = new StateParser();
		
		$exceptions = 0;
		
		foreach ($tests as $test)
		{
			try
			{
				$this->parser->parseDocBlock('/** ' . $test . ' */');
			}
			catch (ParserException $x)
			{
				++$exceptions;
			}
		}
		
		$this->assertSame(count($tests), $exceptions, sprintf(
			'Expected [%d] exceptions, got [%d].', count($tests), $exceptions
		));
	}
	
	/** Quick, short description test. */
	function testParseDocBlockParsesQuickShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Quick, short description test.');
	}
	
	/**
	 * Short description test.
	 */
	function testParseDocBlockParsesShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short description test.');
	}
	
	/**
	 * Short description \@test.
	 */
	function testParseDocBlockParsesShortDescriptionWithEscapedAtSign()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short description @test.');
	}
	
	/**
	 * Short description {@link test}.
	 */
	function testParseDocBlockParsesShortDescriptionWithInlineTag()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short description {@link test}.');
	}
	
	/**
	 * Short description test.
	 * Lorem ipsum dolor sit amet...
	 * Short description continues.
	 */
	function testParseDocBlockParsesMultilineShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short description test. Lorem ipsum dolor sit amet... ' .
			'Short description continues.');
	}
	
	/**
	 * 2*2*2=8
	 */
	function testParseDocBlockParsesShortDescriptionWithAsterixChar()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'2*2*2=8');
	}
	
	/**
	 * Short.
	 * 
	 * Long description test.
	 */
	function testParseDocBlockParsesOneLineLongDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', 'Long description test.');
	}
	
	/**
	 * Short.
	 * 
	 * Long description @test.
	 */
	function testParseDocBlockParsesLongDescriptionContainingAtSign()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', 'Long description @test.');
	}
	
	/**
	 * Short.
	 * 
	 * Long description {@link test}.
	 */
	function testParseDocBlockParsesLongDescriptionWithInlineTag()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', 'Long description {@link test}.');
	}
	
	/**
	 * Short.
	 * 
	 * Long description test.
	 * Same paragraph.
	 * 
	 * New paragraph.
	 */
	function testParseDocBlockParsesLongDescriptionWithMultipleParagraphs()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', "Long description test. Same paragraph.\nNew paragraph.");
	}
	
	/**
	 * 2*2*2=8
	 * 
	 * *************
	 * *HELLO WORLD*
	 * *************
	 */
	function testParseDocBlockParsesLongDescriptionWithAsterixChar()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'2*2*2=8', '************* *HELLO WORLD* *************');
	}
	
	/** @test @test 1 @foo hello world */
	function testParseDocBlockParsesQuickTags()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__), null, null,
			array('test' => array(null, '1'), 'foo' => 'hello world'));
	}
	
	/**
	 * @test
	 * @test 1
	 * @foo hello world
	 */
	function testParseDocBlockParsesTags()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__), null, null,
			array('test' => array(null, '1'), 'foo' => 'hello world'));
	}
	
	/**
	 * @test
	 * 1
	 * @test 2
	 * 2
	 * @test 3
	 * 3
	 * 3
	 * @param string? $foo
	 *        Lorem ipsum dolor sit amet.
	 *        Tema tis rolod muspi merol.
	 */
	function testParseDocBlockParsesTagsWithMultilineValues()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__), null, null,
			array(
				'test'  => array('1', "2\n2", "3\n3\n3"),
				'param' => array(
					"string? \$foo\n       Lorem ipsum dolor sit amet.\n       " .
					"Tema tis rolod muspi merol.")));
	}
	
	/** Short. @test */
	function testParseDocBlockParsesTagAfterQuickShortDescriptionIfNotEscaped()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', null, array('test' => array(null)));
	}
	
	/** Short. @test value */
	function testParseDocBlockParsesTagWithValueAfterQuickShortDescriptionIfNotEscaped()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', null, array('test' => array('value')));
	}
	
	/** Short. @test @test value @foo */
	function testParseDocBlockParsesTagsAfterQuickShortDescriptionIfNotEscaped()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.',
			null,
			array('test' => array(null, 'value'), 'foo' => array(null)));
	}
	
	/** Short. \@escapedTag @unescapedTag */
	function testParseDocBlockIgnoresEscapedTagsAfterQuickShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short. @escapedTag', null, array('unescapedTag' => array(null)));
	}
	
	/**
	 * Short.
	 * @test 1
	 * @test 2
	 */
	function testParseDocBlockParsesTagsRightAfterShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', null, array('test' => array('1', '2')));
	}
	
	/**
	 * Short.
	 *
	 * @test 1
	 * @test 2
	 */
	function testParseDocBlockParsesTagsAfterShortDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', null, array('test' => array('1', '2')));
	}
	
	/**
	 * Short.
	 * 
	 * Long.
	 * @test 1
	 * @test 2
	 */
	function testParseDocBlockParsesTagsRightAfterLongDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', 'Long.', array('test'=> array('1', '2')));
	}
	
	/**
	 * Short.
	 * 
	 * Long.
	 *
	 * @test 1
	 * @test 2
	 */
	function testParseDocBlockParsesTagsAfterLongDescription()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			'Short.', 'Long.', array('test'=> array('1', '2')));
	}
	
	/**
	 * @author morkai <lukasz@walukiewicz.eu>
	 */
	function testParseDocBlockParsesTagsWithAtSignInValueRightAfterNonWhitespaceChar()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__), null, null,
			array('author' => array('morkai <lukasz@walukiewicz.eu>')));
	}
	
	/**
	 * @author morkai <lukasz \@walukiewicz.eu>
	 */
	function testParseDocBlockParsesTagsWithEscapedAtSignInValueRightAfterWhitespaceChar()
	{
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__), null, null,
			array('author' => array('morkai <lukasz @walukiewicz.eu>')));
	}
	
	/** @Foo-Bar() */
	function testParseDocBlockThrowsExceptionIfAnnotationNameContainsNonAlnumOrUnderscoreChars()
	{
		$this->setExpectedException('ParserException');
		
		$this->getDocBlockDef(__FUNCTION__);
	}
	
	/** @Fake<1MoreFake> */
	function testParseDocBlockThrowsExceptionIfAnnotationTypeDoesNotStartWithAlphaOrUnderscore()
	{
		$this->setExpectedException('ParserException');
		
		$this->getDocBlockDef(__FUNCTION__);
	}
	
	/** @Fake<More-Fake> */
	function testParseDocBlockThrowsExceptionIfAnnotationTypeContainsNonAlnumOrUnderscoreChars()
	{
		$this->setExpectedException('ParserException');
		
		$this->getDocBlockDef(__FUNCTION__);
	}
	
	/** @Fake[1nvalid_param] */
	function testParseDocBlockThrowsExceptionIfAnnotationParamDoesNotStartWithAlphaOrUnderscore()
	{
		$this->setExpectedException('ParserException');
		
		$this->getDocBlockDef(__FUNCTION__);
	}
	
	/** @Fake[invalid~param] */
	function testParseDocBlockThrowsExceptionIfAnnotationParamContainsNonAlnumOrUnderscoreChars()
	{
		$this->setExpectedException('ParserException');
		
		$this->getDocBlockDef(__FUNCTION__);
	}
	
	/** @FooBar */
	function testParseDocBlockParsesSimpleAnnotation()
	{
		$this->expect->never($this->arrayParser);
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(new AnnotationDefinition('FooBar')));
	}
	
	/** @Fake<MoreFake> */
	function testParseDocBlockParsesAnnotationWithType()
	{
		$this->expect->never($this->arrayParser);
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(new AnnotationDefinition('Fake', 'MoreFake')));
	}
	
	/** @Fake[param] */
	function testParseDocBlockParsesAnnotationWithParameter()
	{
		$this->expect->never($this->arrayParser);
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, 'param')));
	}
	
	/** @Fake<MoreFake>[param] */
	function testParseDocBlockParsesAnnotationWithTypeAndParameter()
	{
		$this->expect->never($this->arrayParser);
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', 'MoreFake', 'param')));
	}
	
	/** @Fake() */
	function testParseDocBlockParsesAnnotationWithNoValue()
	{
		$this->expect->never($this->arrayParser);
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(new AnnotationDefinition('Fake')));
	}
	
	/** @Fake(1, 2, 3) */
	function testParseDocBlockParsesAnnotationWithArguments()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray('{1, 2, 3}')->returns(array(1, 2, 3));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array(1, 2, 3))));
	}
	
	/** @Fake(bar=1, baz=2) */
	function testParseDocBlockParsesAnnotationWithProperties()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray('{bar=1, baz=2}')->returns(array('bar' => 1, 'baz' => 2));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition(
					'Fake', null, null, array(), array('bar' => 1, 'baz' => 2))));
	}
	
	/** @Fake(1, bar=1, 2, baz=2, 3) */
	function testParseDocBlockParsesAnnotationWithArgumentsAndProperties()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray('{1, bar=1, 2, baz=2, 3}')
				->returns(array(1, 'bar' => 1, 2, 'baz' => 2, 3));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition(
					'Fake', null, null, array(1, 2, 3), array('bar' => 1, 'baz' => 2))));
	}
	
	/** @Fake<MoreFake> @Fake[param] @MoreFake('foo', bar='bar') @FooBar */
	function testParseDocBlockParsesMultipleQuickAnnotations()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{'foo', bar='bar'}")
				->returns(array('foo', 'bar' => 'bar'));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', 'MoreFake'),
				new AnnotationDefinition('Fake', null, 'param'),
				new AnnotationDefinition(
					'MoreFake', null, null, array('foo'), array('bar' => 'bar')),
				new AnnotationDefinition('FooBar')));
	}
	
	/** @Fake('\'foo\'') @Fake('\\\'') */
	function testParseDocBlockParsesEscapedAnnotationStringValues()
	{
		$bs = '\\';
		
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{'$bs'foo$bs''}")->returns(array("'foo'"))
			->exactly(1)->of($this->arrayParser)
				->parseArray("{'$bs$bs$bs''}")->returns(array("$bs'"));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array("'foo'")),
				new AnnotationDefinition('Fake', null, null, array("$bs'"))));
	}
	
	/** @Fake("@Fake()") */
	function testParseDocBlockParsesAnnotationStringValuesWithParentheses()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray('{"@Fake()"}')->returns(array('@Fake()'));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array("@Fake()"))));
	}
	
	/** @Fake(0, -1.2, 0xFF, 0123) */ 
	function testParseDocBlockParsesAnnotationNumericValues()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{0, -1.2, 0xFF, 0123}")->returns(array(0, -1.2, 255, 83));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array(0, -1.2, 255, 83))));
	}
	
	/** @Fake(TRUE, yes, on, FALSE, No, oFf) */
	function testParseDocBlockParsesAnnotationBooleanValues()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{TRUE, yes, on, FALSE, No, oFf}")
				->returns(array(true, true, true, false, false, false));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition(
					'Fake', null, null, array(true, true, true, false, false, false))));
	}
	
	/** @Fake(StateParserTest.TEST) */ 
	function testParseDocBlockParsesAnnotationConstantValues()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{StateParserTest.TEST}")
				->returns(array(StateParserTest::TEST));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition(
					'Fake', null, null, array(StateParserTest::TEST))));
	}
	
	/** @Fake(StateParserTest.class) */ 
	function testParseDocBlockParsesAnnotationClassValues()
	{
		$ref = new ReflectionClass(__CLASS__);
		
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{StateParserTest.class}")
				->returns(array(new ReflectionClass(__CLASS__)));
		
		$def = $this->getDocBlockDef(__FUNCTION__);
		
		$this->assertType('DocBlockDefinition', $def);
		$this->assertTrue(isset($def->annotationDefinitions[0]->arguments[0]));
		
		$arg = $def->annotationDefinitions[0]->arguments[0];
		
		$this->assertType('ReflectionClass', $arg);
		$this->assertSame(__CLASS__, $arg->getName());
	}
	
	/** @Fake([1, 2, 3]) */
	function testParseDocBlockParsesAnnotationListValues()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{[1, 2, 3]}")->returns(array(array(1, 2, 3)));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array(array(1, 2, 3)))));
	}
	
	/** @Fake({one: 1, two: 2, three: 3}) */
	function testParseDocBlockParsesAnnotationMapValues()
	{
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{{one: 1, two: 2, three: 3}}")
				->returns(array(array('one' => 1, 'two' => 2, 'three' => 3)));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition(
					'Fake',
					null,
					null,
					array(array('one' => 1, 'two' => 2, 'three' => 3)))));
	}
	
	/** @Fake([{one: []}, {two: []}, {three: []}]) */
	function testParseDocBlockParsesAnnotationNestedArrayValues()
	{
		$arg = array
		(
			array('one'   => array()),
			array('two'   => array()),
			array('three' => array()),
		);
		
		$this->expect
			->exactly(1)->of($this->arrayParser)
				->parseArray("{[{one: []}, {two: []}, {three: []}]}")
				->returns(array($arg));
		
		$this->assertDocBlockDef($this->getDocBlockDef(__FUNCTION__),
			null, null, array(), array(
				new AnnotationDefinition('Fake', null, null, array($arg))));
	}
	
	/**
	 * @param DocBlockDefinition $def
	 * @param string? $shortDesc
	 * @param string? $longDesc
	 * @param array<string,array> $tags
	 * @param AnnotationDefinition[] $annotations
	 */
	private function assertDocBlockDef(
		DocBlockDefinition $def,
		$shortDesc = null,
		$longDesc = null,
		array $tags = array(),
		array $annotations = array())
	{
		$this->assertSame($shortDesc, $def->shortDescription,
			'Short description mismatch.');
		$this->assertSame($longDesc, $def->longDescription,
			'Long description mismatch.');
		
		$i = 0;
		
		foreach ($tags as $name => $docStrings)
		{
			foreach ((array)$docStrings as $docString)
			{
				$this->assertSame($name, $def->tagDefinitions[$i]->name);
				$this->assertSame($docString, $def->tagDefinitions[$i]->docString);
				
				++$i;
			}
		}
		
		foreach ($annotations as $i => $annotation)
		{
			$this->assertSame(serialize($annotation),
				serialize($def->annotationDefinitions[$i]),
				'Annotation defs mismatch.');
		}
	}
	
	/**
	 * @param  string $method
	 * @return DocBlockDefinition
	 */
	private function getDocBlockDef($method)
	{
		return $this->parser->parseDocBlock($this->getDocComment($method));
	}
	
	/**
	 * @param  string $method
	 * @return string
	 */
	private function getDocComment($method)
	{
		if ($this->reflection === null)
		{
			$this->reflection = new ReflectionObject($this);
		}
		
		return $this->reflection->getMethod($method)->getDocComment();
	}
}